package jmine.tec.persist.schema.impl.dialects;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import javax.persistence.GenerationType;

import jmine.tec.persist.schema.api.IndexOrder;
import jmine.tec.persist.schema.api.SchemaColumn;
import jmine.tec.persist.schema.api.SchemaElement;
import jmine.tec.persist.schema.api.SqlDialect;
import jmine.tec.utils.Tuple;

import org.hibernate.dialect.Dialect;

/**
 * Classe abstrata dos dialetos utilizados para geração de schema pelo jTec. Delega o quanto for possível para o dialeto de hibernate dado.
 * O que não é possível <br/>
 * aproveitar do dialect é implementado de acordo com o dialeto do Oracle10g. <br/>
 * 
 * @author lundberg
 */
public abstract class AbstractSqlDialect implements SqlDialect {

    private Dialect dialect;

    private boolean forceSequenceSupport;

    private boolean assignTablespaces;

    /**
     * Construtor
     * 
     * @param dialect dialeto do hibernate correspondente ao banco de dados desejado.
     */
    protected AbstractSqlDialect(Dialect dialect) {
        this.dialect = dialect;
        this.forceSequenceSupport = true;
        this.assignTablespaces = false;
    }

    /**
     * {@inheritDoc}
     */
    public void setAssignTablespaces(boolean assignTablespaces) {
        this.assignTablespaces = assignTablespaces;
    }

    /**
     * {@inheritDoc}
     */
    public void setForceSequenceSupport(boolean forceSequenceSupport) {
        this.forceSequenceSupport = forceSequenceSupport;
    }

    /**
     * {@inheritDoc}
     */
    public List<String> renderAlternativeKey(String table, String name, List<String> columns) {
        if (this.dialect.supportsUnique()) {
            return this.format("ALTER TABLE %s ADD CONSTRAINT %s UNIQUE (%s)", table, name, this.join(columns, ", "));
        } else {
            return Collections.emptyList();
        }
    }

    /**
     * {@inheritDoc}
     */
    public List<String> renderCheckConstraint(String table, String name, int typeCode, String condition) {
        if (this.dialect.supportsTableCheck()) {
            return this.format("ALTER TABLE %s ADD CONSTRAINT %s CHECK (%s)", table, name, condition);
        } else {
            return Collections.emptyList();
        }
    }

    /**
     * {@inheritDoc}
     */
    public List<String> renderComment(String table, String comment) {
        if (this.dialect.supportsCommentOn()) {
            return this.format("COMMENT ON TABLE %s IS '%s'", table, this.createCommentLiteral(comment));
        } else {
            return Collections.emptyList();
        }
    }

    /**
     * {@inheritDoc}
     */
    public List<String> renderComment(String table, String column, String comment) {
        if (this.dialect.supportsCommentOn()) {
            return this.format("COMMENT ON COLUMN %s.%s IS '%s'", table, column, this.createCommentLiteral(comment));
        } else {
            return Collections.emptyList();
        }
    }

    /**
     * Cria o literal do comentário
     * 
     * @param comment comment
     * @return comentário literal
     */
    protected String createCommentLiteral(String comment) {
        return comment.replaceAll("'", "''");
    }

    /**
     * {@inheritDoc}
     */
    public List<String> renderForeignKey(String foreignKeyName, String referencingTable, String referencedTable,
            String[] referencingColumns, String[] referencedColumns, boolean onDeleteCascade) {
        String cascade = onDeleteCascade ? " ON DELETE CASCADE" : "";
        if (referencedColumns == null) {
            return this.format("ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s%s", referencingTable, foreignKeyName,
                    this.join(Arrays.asList(referencingColumns), ", "), referencedTable, cascade);
        } else {
            return this.format("ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s", referencingTable, foreignKeyName,
                    this.join(Arrays.asList(referencingColumns), ", "), referencedTable, this.join(Arrays.asList(referencedColumns), ", "),
                    cascade);
        }
    }

    /**
     * {@inheritDoc}
     */
    public List<String> renderIndex(String tablespace, String table, String name, List<Tuple<String, IndexOrder>> indexedColumns) {
        String tablespaceSuffix;
        if (this.assignTablespaces && tablespace != null && tablespace.length() != 0) {
            tablespaceSuffix = this.getIndexTablespaceSuffix(tablespace);
        } else {
            tablespaceSuffix = "";
        }
        return this.format("CREATE INDEX %s ON %s (%s)%s", name, table, this.join(this.createIndexColumns(indexedColumns), ", "),
                tablespaceSuffix);
    }

    /**
     * Determina o sufixo a ser adicionado a um statement de criação de índice para criar o índice em um tablespace específico.
     * 
     * @param tablespace tablespace
     * @return sufixo sufixo
     */
    protected String getIndexTablespaceSuffix(String tablespace) {
        return "";
    }

    /**
     * {@inheritDoc}
     */
    protected List<String> createIndexColumns(List<Tuple<String, IndexOrder>> indexedColumns) {
        List<String> columnStrings = new LinkedList<String>();
        for (Tuple<String, IndexOrder> indexedColumn : indexedColumns) {
            switch (indexedColumn.tail()) {
            case UNDEFINED:
                columnStrings.add(indexedColumn.head());
                break;
            case ASC:
                columnStrings.add(String.format("%s ASC", indexedColumn.head()));
                break;
            case DESC:
                columnStrings.add(String.format("%s DESC", indexedColumn.head()));
                break;
            default:
                throw new UnsupportedOperationException();
            }
        }
        return columnStrings;
    }

    /**
     * {@inheritDoc}
     */
    public List<String> renderPrimaryKey(String table, String name, List<? extends SchemaColumn> columns) {
        if (columns.size() == 1 && this.isIdentity(columns.get(0).getGenerationStrategy())) {
            return this.renderIdentityPrimaryKey(table, name, columns.get(0));
        } else {
            List<String> names = new ArrayList<String>(columns.size());
            for (SchemaColumn column : columns) {
                names.add(column.getName());
            }
            return this.format("ALTER TABLE %s ADD CONSTRAINT %s PRIMARY KEY (%s)", table, name, this.join(names, ", "));
        }
    }

    /**
     * {@inheritDoc}
     */
    protected List<String> renderIdentityPrimaryKey(String table, String name, SchemaColumn column) {
        throw new UnsupportedOperationException();
    }

    /**
     * {@inheritDoc}
     */
    public List<String> renderSequence(String name, Integer initialValue) {
        if (this.dialect.supportsSequences()) {
            if (initialValue == null) {
                return this.format("CREATE SEQUENCE %s", name);
            } else {
                return this.format("CREATE SEQUENCE %s START WITH %s", name, initialValue.toString());
            }
        } else {
            return Collections.emptyList();
        }
    }

    /**
     * {@inheritDoc}
     */
    public List<String> renderTable(String tablespace, String name, Collection<? extends SchemaElement> columns) {
        String tablespaceSuffix;
        if (this.assignTablespaces && tablespace != null && tablespace.length() != 0) {
            tablespaceSuffix = this.getCreateTableTablespaceSuffix(tablespace);
        } else {
            tablespaceSuffix = "";
        }
        return this.format("%s %s (\n\t%s\n)%s%s", this.dialect.getCreateTableString().toUpperCase(), name,
                this.join(this.renderElements(columns), ",\n\t"), this.dialect.getTableTypeString(), tablespaceSuffix);
    }

    /**
     * Cria o sufixo do statement de create table que designa o tablespace adequado. Deve ser sobrescrito para dialetos que dão suporte a
     * tal recurso.
     * 
     * @param tablespace tablespace
     * @return sufixo
     */
    protected String getCreateTableTablespaceSuffix(String tablespace) {
        return "";
    }

    /**
     * {@inheritDoc}
     */
    public String renderColumn(String name, SchemaElement type, boolean nullable, String defaultValue) {
        String suffix = this.createColumnSuffix(nullable, defaultValue);
        List<String> typeStatements = type.createStatements(this);
        if (typeStatements.size() != 1) {
            throw new IllegalStateException();
        }
        return String.format("%s %s%s", name, typeStatements.get(0), suffix);
    }

    /**
     * Cria o sufixo da coluna, que define se ela aceita valores nulos e seu valor default
     * 
     * @param nullable nullable
     * @param defaultValue defaultValue
     * @return suffix
     */
    protected String createColumnSuffix(boolean nullable, String defaultValue) {
        if (defaultValue == null) {
            return nullable ? this.dialect.getNullColumnString().toUpperCase() : " NOT NULL";
        } else {
            return String.format(" DEFAULT %s", defaultValue);
        }
    }

    /**
     * Suffixo que pode ser necessário à algumas colunas no caso de algumas estratégias de geração.
     * 
     * @param generationStrategy generationStrategy
     * @return String
     */
    protected boolean isIdentity(GenerationType generationStrategy) {
        if (this.forceSequenceSupport && this.dialect.supportsSequences()) {
            return false;
        } else {
            return this.dialect.supportsIdentityColumns()
                    && (GenerationType.AUTO.equals(generationStrategy) || GenerationType.IDENTITY.equals(generationStrategy));
        }
    }

    /**
     * {@inheritDoc}
     */
    public String renderType(int code, int length, int precision, int scale) {
        return this.dialect.getTypeName(code, length, precision, scale).toUpperCase();
    }

    /**
     * {@inheritDoc}
     */
    public final List<String> renderElements(Collection<? extends SchemaElement> elements) {
        List<String> statements = new LinkedList<String>();
        for (SchemaElement element : elements) {
            statements.addAll(element.createStatements(this));
        }
        return statements;
    }

    /**
     * Método utilitário que invoca String.format com os argumentos dados e encapsula o resultado em uma lista de 1 elemento.
     * 
     * @param format formato
     * @param args argumentos
     * @return lista contando o elemento resultante
     */
    protected List<String> format(String format, String... args) {
        return Collections.singletonList(String.format(format, (Object[]) args));
    }

    /**
     * Une um conjunto de tokens, colocando entre eles uma separação.
     * 
     * @param tokens partes que devem ser unidas
     * @param separation string que deve ser colocada entre cada parte
     * @return resultado da união
     */
    protected String join(Collection<String> tokens, String separation) {
        StringBuilder builder = new StringBuilder();
        Iterator<String> it = tokens.iterator();
        while (it.hasNext()) {
            builder.append(it.next());
            if (it.hasNext()) {
                builder.append(separation);
            }
        }
        return builder.toString();
    }

    /**
     * {@inheritDoc}
     * 
     * @see jmine.tec.persist.schema.api.SqlDialect#postProcessSchema(java.util.List, java.util.Collection)
     */
    public List<String> postProcessSchema(List<String> schemaStatements, Collection<? extends SchemaElement> elements) {
        return schemaStatements;
    }

    /**
     * {@inheritDoc}
     */
    public String renderLiteral(int code, String defaultValue) {
        return defaultValue;
    }
}
